package at.jku.pervasive.recognition;

import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.commons.collections.Bag;
import org.apache.commons.collections.bag.HashBag;

import at.jku.pervasive.Helper;
import at.jku.pervasive.Position;
import at.jku.pervasive.TagAssignment;
import at.jku.pervasive.TagPositionListener;

/**
 * This class handles the recognition of gestures.
 * @author clemens
 */
public class GestureRecognizer implements TagPositionListener
{

	private static final String REFERENCE_POSITION_ID = "reference_position";
	private final List<GestureListener> gestureListeners;
	private final LinkedList<Gesture> recentGestures;
	private final Position referencePosition;
	private final Map<TagAssignment, Position> filteredTagPositions;

	private GestureDetector gestureDetector;
	private GestureType lastRecognizedGestureType;

	private long gestureRecognitionTime = 1000;
	private double gestureRecognitionTreshold = 0.8d;
	private double zOffset = 0.5d;

	private long lastGestureTimestamp;

	public GestureRecognizer()
	{
		super();

		this.gestureListeners = new LinkedList<GestureListener>();
		this.recentGestures = new LinkedList<Gesture>();
		this.filteredTagPositions = new TreeMap<TagAssignment, Position>();

		this.gestureDetector = new SimpleGestureDetector();

		this.referencePosition = new Position();
		this.referencePosition.setId(REFERENCE_POSITION_ID);
	}

	public void addGestureListener(GestureListener gestureListener)
	{
		gestureListeners.add(gestureListener);
	}

	@Override
	public void filteredTagPositionChanged(TagAssignment tagAssignment, Position newPosition)
	{
		// add position to recent tag positions
		this.filteredTagPositions.put(tagAssignment, newPosition);

		// check if all tag positions (left, right, front and back) are
		// available; and if so, we can try to extract a gesture out of the
		// positions
		if (this.filteredTagPositions.size() < 4)
			return;

		// recalculate the reference position
		recalculateReferencePosition();

		// extract gestureType out of front, reference and right position
		Position left = this.filteredTagPositions.get(TagAssignment.LEFT);
		Position right = this.filteredTagPositions.get(TagAssignment.RIGHT);
		GestureType gestureType = gestureDetector.getRecognizedGesture(left, referencePosition, right);

		// recognize gesture
		recognizeGesture(gestureType);
	}

	public void removeGestureListener(GestureListener gestureListener)
	{
		gestureListeners.remove(gestureListener);
	}

	public void setGestureRecognitionTime(long gestureRecognitionTime)
	{
		this.gestureRecognitionTime = gestureRecognitionTime;
	}

	public long getGestureRecognitionTime()
	{
		return gestureRecognitionTime;
	}

	public void setGestureRecognitionTreshold(double gestureRecognitionTreshold)
	{
		this.gestureRecognitionTreshold = gestureRecognitionTreshold;
	}

	public double getGestureRecognitionTreshold()
	{
		return gestureRecognitionTreshold;
	}

	public void setZOffset(double zOffset)
	{
		this.zOffset = zOffset;
	}

	public double getZOffset()
	{
		return zOffset;
	}

	@Override
	public void tagPositionChanged(TagAssignment tagAssignment, Position filteredPosition)
	{
		// not implemented - because this gesture recognizer uses the filtered
		// positions; see #filteredTagPositionChanged
	}

	private void recalculateReferencePosition()
	{
		Position front = this.filteredTagPositions.get(TagAssignment.FRONT);
		Position back = this.filteredTagPositions.get(TagAssignment.BACK);

		// calculate mean of the two positions
		double x = front.getX() + back.getX();
		double y = front.getY() + back.getY();
		double z = front.getZ() + back.getZ();

		x /= 2.0;
		y /= 2.0;
		z /= 2.0;

		// add offset to Z
		z += zOffset;

		referencePosition.setDate(Helper.now());
		referencePosition.setX(x);
		referencePosition.setY(y);
		referencePosition.setZ(z);
	}

	private void recognizeGesture(GestureType gestureType)
	{
		// add gesture to recently occured gestures
		recentGestures.add(new Gesture(gestureType));

		// trim recent gestures list and check if the list contained gestures
		// older than the MINIMUM_GESTURE_DURATION
		boolean removed = trimRecentGesturesList();
		if (!removed)
			return;

		// get the gesture, which occured most frequently over the last
		// MINUMUM_GESTURE_DURATION
		// calculate occurrences
		Bag histogram = new HashBag();
		for (Gesture gesture : recentGestures)
		{
			histogram.add(gesture.getGestureType());
		}

		int max = 0;
		GestureType mostFrequentGesture = null;
		for (Object object : histogram)
		{
			GestureType tmpGestureType = (GestureType) object;
			int count = histogram.getCount(tmpGestureType);
			if (count > max)
			{
				max = count;
				mostFrequentGesture = tmpGestureType;
			}
		}

		double size = recentGestures.size();
		double percent = max / size;

		if (percent >= gestureRecognitionTreshold)
		{
			if (lastRecognizedGestureType != mostFrequentGesture)
			{
				// inform listeners that lastGestureType has been finished -
				// only if the last gesture is not null or NONE
				if (lastRecognizedGestureType != null && GestureType.NONE != lastRecognizedGestureType)
				{
					long duration = Helper.timeDiff(lastGestureTimestamp);
					for (GestureListener gestureListener : gestureListeners)
					{
						gestureListener.gestureFinished(lastRecognizedGestureType, duration);
					}
				}

				lastRecognizedGestureType = mostFrequentGesture;

				// inform listeners that a new gesture type has been started
				Date now = Helper.now();
				lastGestureTimestamp = now.getTime();

				Position left = filteredTagPositions.get(TagAssignment.LEFT);
				Position right = filteredTagPositions.get(TagAssignment.RIGHT);
				GestureCharacteristics gC = new GestureCharacteristics(left, referencePosition, right);

				for (GestureListener gestureListener : gestureListeners)
				{
					gestureListener.gestureStarted(lastRecognizedGestureType, now);
					gestureListener.gestureRecognized(lastRecognizedGestureType, now, gC);
				}

				recentGestures.clear();
			}
		}
	}

	private boolean trimRecentGesturesList()
	{
		// delete gestures which are older than the gesture recognition interval
		boolean removed = false;
		Iterator<Gesture> descendingIterator = recentGestures.descendingIterator();
		while (descendingIterator.hasNext())
		{
			Gesture gesture = descendingIterator.next();
			if (Helper.isOlderThan(gesture.getTimestamp(), gestureRecognitionTime))
			{
				descendingIterator.remove();
				removed = true;
			}
		}

		return removed;
	}
}
